package jadeCW;

import jade.core.AID;
import jade.core.Agent;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.SearchConstraints;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.lang.acl.ACLMessage;
import jade.proto.SubscriptionInitiator;
import jade.util.leap.Iterator;
import jadeCW.behaviour.FindAppointmentOwner;
import jadeCW.behaviour.RepondToProposal1;
import jadeCW.behaviour.RequestAppointment;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Patient agent class
 * 
 * @author br08, dac108, dv08
 */
public class PatientAgent extends Agent {
	/**
	 * Generated serialisation ID
	 */
	private static final long serialVersionUID = 3730277545456113313L;

	/**
	 * The appointment value indicating the patient has not yet been assigned an
	 * appointment
	 */
	public static final int NO_APPOINTMENT = 0;

	/**
	 * The current allocated appointment number
	 */
	private int appointment = NO_APPOINTMENT;
	/**
	 * Map of appointment number to preference (higher is stronger preference)
	 */
	private Map<Integer, Integer> preferences = new HashMap<Integer, Integer>();
	/**
	 * Map of appointment number to preference for the current appointment
	 * finding round
	 */
	private Map<Integer, Integer> currentPreferences;
	/**
	 * List of appointment allocators (hospitals)
	 */
	private List<DFAgentDescription> allocators = new LinkedList<DFAgentDescription>();
	/**
	 * ID of the agent holding a preferred appointment
	 */
	private AID preferredAppointmentOwner;
	/**
	 * The preferred appointment number
	 */
	private int preferredAppointment;
	/**
	 * A lock for when patients are in the middle of requests
	 */
	private boolean locked;

	/**
	 * @return The currently allocated appointment
	 */
	public int getAppointment() {
		return this.appointment;
	}

	/**
	 * Allocates the appointment to the patient
	 * 
	 * @param appointment
	 *            The appointment to allocate
	 */
	public void setAppointment(int appointment) {
		this.appointment = appointment;

		// add the behaviours to find better appointments and respond to swaps
		addBehaviour(new FindAppointmentOwner());
		addBehaviour(new RepondToProposal1());
	}

	/**
	 * Checks that the patient has a valid allocated appointment
	 * 
	 * @return True if the patient has been allocated an appointment
	 */
	public boolean hasAppointment() {
		return (getAppointment() != NO_APPOINTMENT);
	}

	/**
	 * @return The list of agents the patient has discovered who can allocate
	 *         appointments (hospitals)
	 */
	public List<DFAgentDescription> getAppointmentAllocators() {
		return this.allocators;
	}

	/**
	 * Get the preference for the given appointment
	 * 
	 * @param appointment
	 *            The appointment the check
	 * @return The preference value. Higher is strong preference, 0 indicates
	 *         lowest preference
	 */
	public int getPreference(int appointment) {
		if (this.currentPreferences.containsKey(appointment)) {
			return this.currentPreferences.get(appointment);
		}
		return 0;
	}

	/**
	 * Gets the most preferred appointment from the given appointment
	 * 
	 * @param appointment
	 *            The appointment to try and find better than
	 * @return The most preferred appointment (could be the same if there are no
	 *         more preferred appointments)
	 */
	public int getMostPreferredAppointment(int appointment) {
		int preference = this.getPreference(appointment);
		int preferred = appointment;

		// check each preference
		for (int hasPreference : this.currentPreferences.keySet()) {
			int newPreference = this.getPreference(hasPreference);

			// if the new one is higher than the current highest, use that
			// instead
			if (newPreference > preference) {
				preferred = hasPreference;
				preference = newPreference;
			}
		}

		return preferred;
	}

	/**
	 * Removes the preference for a given appointment (preference will become 0
	 * for that appointment)
	 * 
	 * @param appointment
	 *            The appointment to remove the preference for
	 */
	public void removePreference(int appointment) {
		this.currentPreferences.remove(appointment);
	}

	/**
	 * Resets the current working preferences to the originals
	 */
	public void resetPreferences() {
		this.currentPreferences = new HashMap<Integer, Integer>(
				this.preferences);
	}

	/**
	 * @return True if the patient knows the owner of the preferred appointment
	 */
	public boolean knowsPreferredOwners() {
		return (this.preferredAppointmentOwner != null);
	}

	/**
	 * @return The ID of the owner of the preferred appointment (null if not
	 *         set)
	 */
	public AID getPreferredOwner() {
		return this.preferredAppointmentOwner;
	}

	/**
	 * @return The preferred appointment (0 if not seT)
	 */
	public int getPreferredAppointment() {
		return this.preferredAppointment;
	}

	/**
	 * Sets the preferred appointment and owner
	 * 
	 * @param appointment
	 *            The preferred appointment
	 * @param owner
	 *            The owner of the preferred appointment
	 */
	public void setPreferredAppointment(int appointment, AID owner) {
		this.preferredAppointmentOwner = owner;
		this.preferredAppointment = appointment;
	}

	/**
	 * @param offered
	 *            The offered appointment to check the preference of
	 * @return True if the offered appointment is worse than the currently
	 *         allocated one
	 */
	public boolean isOfferedWorse(int offered) {
		return (getPreference(offered) < getPreference(this.appointment));
	}

	/**
	 * Locks the agent from accepting swap proposals
	 */
	public void lockRequesting() {
		this.locked = true;
	}

	/**
	 * Unlocks the agent from accepting swap proposals
	 */
	public void unlockRequesting() {
		this.locked = false;
	}

	/**
	 * Checks that the agent can accept swap proposals
	 * 
	 * @return True if the agent is currently locked out from accepting swap
	 *         proposals
	 */
	public boolean isRequesting() {
		return this.locked;
	}

	protected void setup() {
		Object[] args = getArguments();

		// if we have some arguments, parse them
		if (args.length > 0) {

			// starting at the initial preference
			int preference = 1;

			// and working backwards (right-to-left) through the list of
			// appointments
			for (int i = args.length - 1; i >= 0; i--) {
				String appointment = args[i].toString();

				// if a hyphen is encountered, the next set of appointments are
				// a higher priority
				if (appointment.equals("-")) {
					++preference;

					continue;
				}

				// map the appointment to the current preference
				this.preferences.put(Integer.parseInt(appointment), preference);
			}
		}
		
		resetPreferences();

		// create the agent description
		DFAgentDescription template = new DFAgentDescription();

		// create the service description
		ServiceDescription templateSd = new ServiceDescription();

		// add the hospital's AllocateAppointment service type to the service
		// description
		templateSd.setType(HospitalAgent.SERVICE_TYPE);

		// add the service description to the agent description
		template.addServices(templateSd);

		// create empty search constraints
		SearchConstraints sc = new SearchConstraints();

		// subscribe to agent publications
		addBehaviour(new SubscriptionInitiator(this, DFService
				.createSubscriptionMessage(this, getDefaultDF(), template, sc)) {
			private static final long serialVersionUID = -3571386940136239439L;

			@Override
			protected void handleInform(ACLMessage inform) {
				try {
					// get the descriptions of the agents the patient is being
					// informed of
					DFAgentDescription[] results = DFService
							.decodeNotification(inform.getContent());

					for (DFAgentDescription agent : results) {
						Iterator it = agent.getAllServices();

						// for each service the agent provides
						while (it.hasNext()) {
							ServiceDescription sd = (ServiceDescription) it
									.next();

							// if it is the AllocateAppointment type, add it to
							// the list of hospitals
							if (sd.getType().equals(HospitalAgent.SERVICE_TYPE)) {
								allocators.add(agent);
							}
						}
					}
				} catch (FIPAException fe) {
					fe.printStackTrace();
				}

				// add the request appointment behaviour now we have a list of
				// allocators
				addBehaviour(new RequestAppointment());
			}
		});
	}

	@Override
	protected void takeDown() {
		String appointment = "null";
		if (hasAppointment()) {
			appointment = "Appointment " + getAppointment();
		}
		System.out.println(this.getLocalName() + ": " + appointment);
		System.out.println();
	}
}
